package com.ncmmall.querychannel;

import com.feijin.commons.lang.Requires;
import com.mysema.query.jpa.JPQLQuery;
import com.mysema.query.jpa.impl.JPAQuery;
import com.mysema.query.types.EntityPath;
import com.mysema.query.types.OrderSpecifier;
import com.mysema.query.types.Predicate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.querydsl.SimpleEntityPathResolver;

import javax.persistence.EntityManager;
import javax.persistence.NoResultException;
import javax.persistence.Query;
import javax.persistence.TypedQuery;
import java.util.List;
import java.util.Map;

/**
 * @author linxs
 */
public abstract class AbstractJpaQueryRepository implements QueryRepository {

    private final Logger LOG = LoggerFactory.getLogger(getClass());

    protected abstract EntityManager getEntityManager();

    protected abstract QueryRepositoryFilter getQueryRepositoryFilter();

    protected abstract String filter(String query, QueryPageRequest QueryPageRequest);

    @Override
    public <T> T findOne(Class<T> clazz, Object primaryKey) {
        return getEntityManager().find(clazz, primaryKey);
    }

    @SuppressWarnings("unchecked")
    @Override
    public <T> T findOne(String sql, Class<T> clazz) {
        Query query = getEntityManager().createQuery(sql, clazz);
        return (T) query.getSingleResult();
    }

    @Override
    public Object findOne(String sql) {
        Query query = getEntityManager().createQuery(sql);
        return query.getSingleResult();
    }

    public <T> T findFirst(String queryString, Map<String, Object> params, Class<T> clazz) {
        Requires.hasText(queryString, "queryString must not be null or emtpty.");
        Requires.notNull(params, "params must not be null.");
        Requires.notNull(clazz, "clazz must not be null.");

        try {
            TypedQuery<T> query = newQuery(queryString, clazz);
            query.setFirstResult(0);
            query.setMaxResults(1);

            return withQueryParams(query, params).getSingleResult();
        } catch (Exception e) {
            throw QueryChannelExceptionHelper.transform(e);
        }
    }


    @SuppressWarnings("unchecked")
    @Override
    public <T> T selectOneDTO(String sql, Map<String, Object> params, JpaQueryUnwrap unwrap) {
        Requires.hasText(sql, "sql must not be null or emtpty.");
        Requires.notNull(params, "params must not be null.");
        Requires.notNull(unwrap, "unwrap clazz not be null.");
        Query query = getEntityManager().createNativeQuery(sql);
        for (Map.Entry<String, Object> entry : params.entrySet()) {
            query.setParameter(entry.getKey(), entry.getValue());
        }
        unwrap.execute(query);

        try {
            return (T) query.getSingleResult();
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    @SuppressWarnings("unchecked")
    @Override
    public Map<String, Object> selectOneMap(String sql, Map<String, Object> params, JpaQueryUnwrap unwrap) {
        Requires.hasText(sql, "sql must not be null or emtpty.");
        Requires.notNull(unwrap, "clazz must not be null.");
        Query query = getEntityManager().createNativeQuery(sql);

        for (Map.Entry<String, Object> entry : params.entrySet()) {
            query.setParameter(entry.getKey(), entry.getValue());
        }
        unwrap.execute(query);
        try {
            return (Map<String, Object>) query.getSingleResult();
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    @Override
    public <T> List<T> queryLimitEntity(String sql, QueryPageRequest pageRequest, Class<T> clazz) {
        Requires.hasText(sql, "sql must not be null or emtpty.");
        Requires.notNull(pageRequest, "pageRequest must not be null.");
        Requires.notNull(clazz, "clazz clazz not be null.");
        Query query = getEntityManager().createNativeQuery(sql);
        withQueryParam(query, pageRequest);
        return query.getResultList();
    }

    @Override
    public <T> QueryPage<T> selectPageDTO(String sql, QueryPageRequest pageRequest, JpaQueryUnwrap unwrap) {
        String countSql = QueryRepositoryHelper.countSql4Mysql(sql);
        Query queryCount = getEntityManager().createNativeQuery(countSql);
        for (Map.Entry<String, Object> entry : pageRequest.getParams().entrySet()) {
            queryCount.setParameter(entry.getKey(), entry.getValue());
        }

        Object object = queryCount.getSingleResult();
        Long totalCount = Long.valueOf(String.valueOf(object));

        Query query = getEntityManager().createNativeQuery(sql);
        withQueryParam(query, pageRequest);
        unwrap.execute(query);

        return new QueryPage<T>(totalCount, query.getResultList(), pageRequest.getPageNo(), pageRequest.getPageSize());
    }

    @Override
    public <T> List<T> selectLimitDTO(String sql, QueryPageRequest pageRequest, JpaQueryUnwrap unwrap) {
        Requires.hasText(sql, "sql must not be null or emtpty.");
        Requires.notNull(pageRequest, "pageRequest must not be null.");
        Requires.notNull(unwrap, "unwrap must not be null.");
        Query query = getEntityManager().createNativeQuery(sql);
        withQueryParam(query, pageRequest);
        unwrap.execute(query);
        return query.getResultList();
    }


    private Query withQueryParam(Query query, QueryPageRequest pageRequest) {
        for (Map.Entry<String, Object> entry : pageRequest.getParams().entrySet()) {
            query.setParameter(entry.getKey(), entry.getValue());
        }
        query.setFirstResult((pageRequest.getPageNo() - 1) * pageRequest.getPageSize()).setMaxResults(pageRequest.getPageSize());
        return query;
    }

    @SuppressWarnings("unchecked")
    @Override
    public <T> List<T> selectAllDTO(String sql, Map<String, Object> params, JpaQueryUnwrap unwrap) {
        Requires.hasText(sql, "sql must not be null or emtpty.");
        Requires.notNull(params, "params must not be null.");
        Requires.notNull(unwrap, "unwrap must not be null.");
        Query query = getEntityManager().createNativeQuery(sql);
        unwrap.execute(query);
        if (null == params) {
            return query.getResultList();
        }

        for (Map.Entry<String, Object> entry : params.entrySet()) {
            query.setParameter(entry.getKey(), entry.getValue());
        }
        return query.getResultList();
    }

    @Override
    public List<Map<String, Object>> selectAllMap(String sql, Map<String, Object> params, JpaQueryUnwrap unwrap) {
        Requires.hasText(sql, "sql must not be null or emtpty.");
        Requires.notNull(unwrap, "clazz must not be null.");
        Query query = getEntityManager().createNativeQuery(sql);
        unwrap.execute(query);

        if (null == params) {
            return query.getResultList();
        }

        for (Map.Entry<String, Object> entry : params.entrySet()) {
            query.setParameter(entry.getKey(), entry.getValue());
        }
        return query.getResultList();
    }

    @Override
    public int selectCount(String sql, Map<String, Object> params) {
        Requires.hasText(sql, "sql must not be null or emtpty.");
        Requires.notNull(params, "params must not be null or emtpty.");
        Query query = getEntityManager().createNativeQuery(sql);
        if (!params.isEmpty()) {
            for (Map.Entry<String, Object> entry : params.entrySet()) {
                query.setParameter(entry.getKey(), entry.getValue());
            }
        }
        return Integer.parseInt(query.getSingleResult().toString());
    }


    @Override
    public Double selectSum(String sql, Map<String, Object> params) {
        Requires.hasText(sql, "sql must not be null or emtpty.");
        Requires.notNull(params, "params must not be null or emtpty.");
        Query query = getEntityManager().createNativeQuery(sql);
        if (!params.isEmpty()) {
            for (Map.Entry<String, Object> entry : params.entrySet()) {
                query.setParameter(entry.getKey(), entry.getValue());
            }
        }
        return Double.valueOf(query.getSingleResult().toString());
    }

    @Override
    public double selectAvg(String sql, Map<String, Object> params) {
        Requires.hasText(sql, "sql must not be null or emtpty.");
        Requires.notNull(params, "params must not be null or emtpty.");
        Query query = getEntityManager().createNativeQuery(sql);
        if (!params.isEmpty()) {
            for (Map.Entry<String, Object> entry : params.entrySet()) {
                query.setParameter(entry.getKey(), entry.getValue());
            }
        }
        Object avg = query.getSingleResult();
        return avg == null ? 0.0 : Double.parseDouble(avg.toString());
    }

    @Override
    public <T> List<T> queryAllEntity(String sql, Map<String, Object> params, Class<T> clazz) {
        Requires.hasText(sql, "sql must not be null or emtpty.");
        Requires.notNull(clazz, "clazz must not be null.");
        Requires.notNull(params, "params must not be null.");
        Query query = getEntityManager().createNativeQuery(sql, clazz);
        if (null == params) {
            return query.getResultList();
        }

        for (Map.Entry<String, Object> entry : params.entrySet()) {
            query.setParameter(entry.getKey(), entry.getValue());
        }
        return query.getResultList();
    }

    @Override
    public <T> T queryOneEntity(String sql, Map<String, Object> params, Class<T> clazz) {
        Requires.hasText(sql, "sql must not be null or emtpty.");
        Requires.notNull(clazz, "clazz must not be null.");
        Requires.notNull(params, "params must not be null.");
        Query query = getEntityManager().createNativeQuery(sql, clazz);
        if (null == params) {
            return (T) query.getSingleResult();
        }

        for (Map.Entry<String, Object> entry : params.entrySet()) {
            query.setParameter(entry.getKey(), entry.getValue());
        }
        return (T) query.getSingleResult();
    }


    @SuppressWarnings("unchecked")
    @Override
    public <T> QueryPage<T> queryPageEntity(String sql, QueryPageRequest pageRequest, Class<T> clazz) {
        String countSql = QueryRepositoryHelper.countSql4Mysql(sql);
        Query queryCount = getEntityManager().createNativeQuery(countSql);
        for (Map.Entry<String, Object> entry : pageRequest.getParams().entrySet()) {
            queryCount.setParameter(entry.getKey(), entry.getValue());
        }

        Object object = queryCount.getSingleResult();
        Long totalCount = Long.valueOf(String.valueOf(object));

        Query query = getEntityManager().createNativeQuery(sql, clazz);
        for (Map.Entry<String, Object> entry : pageRequest.getParams().entrySet()) {
            query.setParameter(entry.getKey(), entry.getValue());
        }

        query.setFirstResult((pageRequest.getPageNo() - 1) * pageRequest.getPageSize()).setMaxResults(pageRequest.getPageSize());
        return new QueryPage<T>(totalCount, query.getResultList(), pageRequest.getPageNo(), pageRequest.getPageSize());
    }

    @Override
    public <T> T findOne(String queryString, Map<String, Object> params, Class<T> clazz) {
        Requires.hasText(queryString, "queryString must not be null or emtpty.");
        Requires.notNull(params, "params must not be null.");
        Requires.notNull(clazz, "clazz must not be null.");

        try {
            return withQueryParams(newQuery(queryString, clazz), params).getSingleResult();
        } catch (NoResultException e) {
            return null;
        }
    }


    @Override
    public <T> List<T> findAll(String queryString, Map<String, Object> params, Class<T> clazz) {
        Requires.hasText(queryString, "queryString must not be null or emtpty.");
        Requires.notNull(params, "params must not be null.");
        Requires.notNull(clazz, "clazz must not be null.");

        try {
            return withQueryParams(newQuery(queryString, clazz), params).getResultList();
        } catch (Exception e) {
            throw QueryChannelExceptionHelper.transform(e);
        }
    }

    @Override
    public <T> List<T> findLimit(String queryString, QueryPageRequest pageRequest, Class<T> clazz) {
        Requires.hasText(queryString, "queryString must not be null or emtpty.");
        Requires.notNull(pageRequest, "pageRequest must not be null.");
        Requires.notNull(clazz, "clazz must not be null.");

        try {
            List<T> result = withQueryModifiers(withQueryParams(newQuery(queryString, clazz), pageRequest.getParams()), pageRequest.getPageNo(), pageRequest.getPageSize()).getResultList();
            return result;
        } catch (Exception e) {
            throw QueryChannelExceptionHelper.transform(e);
        }
    }

    @Override
    public <T> QueryPage<T> findPage(String queryString, QueryPageRequest pageRequest, Class<T> clazz) {
        Requires.hasText(queryString, "queryString must not be null or emtpty.");
        Requires.notNull(pageRequest, "pageRequest must not be null.");
        Requires.notNull(clazz, "clazz must not be null.");

        try {
            if (null != this.getQueryRepositoryFilter()) {
                queryString = this.getQueryRepositoryFilter().invoke(queryString, pageRequest);
            }
            // count result
            Long totalCount = withQueryParams(newQuery(QueryRepositoryHelper.newCountQuery(queryString), Long.class), pageRequest.getParams()).getSingleResult();

            List<T> result = withQueryModifiers(withQueryParams(newQuery(queryString, clazz), pageRequest.getParams()), pageRequest.getPageNo(), pageRequest.getPageSize()).getResultList();

            return new QueryPage<T>(totalCount, result, pageRequest.getPageNo(), pageRequest.getPageSize());
        } catch (Exception e) {
            throw QueryChannelExceptionHelper.transform(e);
        }
    }

    private <T> TypedQuery<T> newQuery(String queryString, Class<T> clazz) {
        return getEntityManager().createQuery(queryString, clazz);
    }

    private <T> TypedQuery<T> withQueryParams(TypedQuery<T> query, Map<String, Object> params) {
        for (Map.Entry<String, Object> entry : params.entrySet()) {
            query.setParameter(entry.getKey(), entry.getValue());
        }
        return query;
    }

    private <T> TypedQuery<T> withQueryModifiers(TypedQuery<T> query, int pageNo, int pageSize) {
        return query.setFirstResult((pageNo - 1) * pageSize).setMaxResults(pageSize);
    }

    private <T> TypedQuery<T> withQueryModifiersLimit(int limit, int count, TypedQuery<T> query, Map<String, Object> params) {
        for (Map.Entry<String, Object> entry : params.entrySet()) {
            query.setParameter(entry.getKey(), entry.getValue());
        }
        return query.setFirstResult(limit).setMaxResults(count);
    }


    @Override
    public <T> List<T> findAll(int limit, int count, String queryString, Map<String, Object> params, Class<T> clazz) {
        Requires.hasText(queryString, "queryString must not be null or emtpty.");
        Requires.notNull(params, "params must not be null.");
        Requires.notNull(clazz, "clazz must not be null.");

        try {
            return withQueryModifiersLimit(limit, count, newQuery(queryString, clazz), params).getResultList();
        } catch (Exception e) {
            throw QueryChannelExceptionHelper.transform(e);
        }
    }

    private static class QueryRepositoryHelper {

        static String countSql4Mysql(String sql) {
            StringBuilder stringBuilder = new StringBuilder(deleteOrderBy(sql));
            stringBuilder.insert(0, "SELECT COUNT(*) FROM ( ");
            stringBuilder.append(" ) AS counttabel");
            return stringBuilder.toString();
        }

        static String deleteOrderBy(String sql) {
            int orderByIndex = sql.toUpperCase().lastIndexOf("ORDER BY");
            int lastRightBracket = sql.toUpperCase().lastIndexOf(")");// 防止聚集函数中的order by
            StringBuilder stringBuilder = new StringBuilder();
            stringBuilder.append(sql);
            if (orderByIndex != -1 && lastRightBracket < orderByIndex) {
                stringBuilder.delete(orderByIndex, sql.length());
            }
            return stringBuilder.toString();
        }

        static String newCountQuery(String queryString) {
            return isQueryContainsDistinct(queryString) ? newDistinctCountQuery(queryString) : newCommonsCountQuery(queryString);
        }

        private static boolean isQueryContainsDistinct(String queryString) {
            return queryString.matches(DISTINCT_PATTERN);
        }

        private static String newDistinctCountQuery(String queryString) {
            String items = getDistinctItems(queryString);
            String fromQuery = getFromQuery(queryString);
            return String.format("select count(distinct %s) %s", items, fromQuery);
        }

        private static String getFromQuery(String queryString) {
            String fromQuery = queryString;
            if (fromQuery.indexOf(" from") != -1) {
                fromQuery = fromQuery.substring(fromQuery.indexOf(" from"));
            }
            if (fromQuery.indexOf(" FROM") != -1) {
                fromQuery = fromQuery.substring(fromQuery.indexOf(" FROM"));
            }
            return fromQuery;
        }

        private static String getDistinctItems(String queryString) {
            return queryString.replaceAll(DISTINCT_PATTERN, "$1");
        }

        private static String newCommonsCountQuery(String queryString) {
            String fromSQL = getFromQuery(queryString);
            return String.format("select count(*) %s", fromSQL);
        }

        private static final String DISTINCT_PATTERN = "(?i)select\\s+distinct\\s(.+)\\sfrom\\s+.+";
    }

    @Override
    public <T> List<T> findAll(Predicate predicate, Class<T> clazz) {
        EntityPath<T> entityPath = this.getEntityPath(clazz);
        return createQuery(new Predicate[]{predicate}, clazz).list(entityPath);
    }

    @Override
    public <T> List<T> findAll(Predicate predicate, OrderSpecifier<?>[] orders, Class<T> clazz) {
        EntityPath<T> entityPath = this.getEntityPath(clazz);
        return createQuery(new Predicate[]{predicate}, clazz).orderBy(orders).list(entityPath);
    }

    @Override
    public <T> T findOne(Predicate predicate, Class<T> clazz) {
        EntityPath<T> entityPath = this.getEntityPath(clazz);

        try {
            return createQuery(new Predicate[]{predicate}, clazz).uniqueResult(entityPath);
        } catch (NoResultException e) {
            return null;
        }
    }

    @Override
    public <T> Long findCount(Predicate predicate, Class<T> clazz) {
        return createQuery(new Predicate[]{predicate}, clazz).count();
    }

    private JPQLQuery createQuery(Predicate[] predicate, Class<?> clazz) {
        EntityPath<?> entityPath = this.getEntityPath(clazz);
        return new JPAQuery(this.getEntityManager()).from(entityPath).where(predicate);
    }

    private <T> EntityPath<T> getEntityPath(Class<T> clazz) {
        return SimpleEntityPathResolver.INSTANCE.createPath(clazz);
    }

}
